/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package coalescent.data;

import coalescent.model.KC64;
import coalescent.model.KC64.EventType;

import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multiset;

import commons.util.ExceptionUtil;

import org.apache.commons.lang.builder.HashCodeBuilder;
import org.openide.util.NbBundle;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Infinite alleles data.
 *
 * @param <A> allele type
 * @param <M> the data model (subtype of {@code KC64})
 * @param <R> the runtime type of this class
 * @author Susanta Tewari
 */
public abstract class KC64Data<A, M extends KC64, R extends KC64Data<A, M, R>>
        extends PopGenData<A, EventType, M, R> {

    /**
     * Initializes the model and the allele frequencies. The allele frequency map is made
     * unmodifiable.
     *
     * @param model the data model
     * @param alleleFreq allele frequency
     */
    public KC64Data(M model, Map<A, Integer> alleleFreq) {
        super(model, alleleFreq);
    }


    // <editor-fold defaultstate="collapsed" desc="Impl: getAlleles">

    /**
     * Method description
     *
     *
     * @param eventType
     *
     * @return
     */
    @Override
    public final ImmutableSet<A> getAlleles(EventType eventType) {

        switch (eventType) {

            case COALESCENT :
                return getCoalescentAlleles();

            case MUTATION :
                return getMutationAlleles();

            default :
                throw ExceptionUtil.throwRuntimeExDefaultEnum(eventType);
        }
    }

    /**
     * Alleles on which the event {@link EventType#COALESCENT} could take place.
     *
     * @return the alleles on which the event {@code COALESCENT} could take place.
     */
    private ImmutableSet<A> getCoalescentAlleles() {

        final ImmutableSet<A> all_alleles = getAlleles();
        Collection<A> coalescent_alleles  = new ArrayList<>(all_alleles.size());

        for (A k : all_alleles) {

            if (getAlleleFrequency(k) > 1) {
                coalescent_alleles.add(k);
            }
        }

        return ImmutableSet.copyOf(coalescent_alleles);
    }

    /**
     * Alleles on which the event {@link EventType#MUTATION} could take place.
     *
     * @return the alleles on which the event {@code MUTATION} could take place.
     */
    private ImmutableSet<A> getMutationAlleles() {

        final ImmutableSet<A> all_alleles = getAlleles();
        Collection<A> mutation_alleles    = new ArrayList<>(all_alleles.size());

        for (A k : all_alleles) {

            if (getAlleleFrequency(k) == 1) {
                mutation_alleles.add(k);
            }
        }

        return ImmutableSet.copyOf(mutation_alleles);
    }


    // </editor-fold>

    /**
     * Throws {@code IllegalArgumentException} if the parameter {@code allele} does not carry an
     * immediate mutation. This method is used for checking methods with a parameter that is
     * supposed to cary an immediate mutation.
     *
     * @param allele allele supposedly carrying an immediate mutation
     */
    protected final void checkMutationAllele(A allele) {

        checkAllelePresent(allele);

        if (!getAlleles(EventType.MUTATION).contains(allele)) {

            final String msg = NbBundle.getMessage(getClass(), "Not_Mutation_Allele", allele);

            throw new IllegalArgumentException(msg);
        }
    }

    /**
     * Two {@code Data} instances are equal if they have equal models and equal allele frequency
     * spectrum.
     *
     * @param obj the object to compare against
     * @return {@code false} if 1) {@code obj} is {@code null} 2) from a different class 3) have
     *         different model objects (based on {@code equals}) 4) have different allele frequency spectrum;
     *         {@code true} otherwise
     */
    @Override
    public boolean equals(Object obj) {

        if (obj == null) {
            return false;
        }

        if (obj == this) {
            return true;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        @SuppressWarnings("unchecked") final KC64Data<A, M, R> other = (KC64Data<A, M, R>) obj;

        if (!getModel().equals(other.getModel())) {
            return false;
        }

        Multiset<Integer> this_allele_freq_spectrum =
            ImmutableMultiset.copyOf(getAlleleFreq().values());
        Multiset<Integer> other_allele_freq_spectrum =
            ImmutableMultiset.copyOf(other.getAlleleFreq().values());

        return this_allele_freq_spectrum.equals(other_allele_freq_spectrum);
    }

    /**
     * Computes hash code based on the data model and allele frequency spectrum.
     *
     * @return hash code based on the data model and allele frequency spectrum
     */
    @Override
    public int hashCode() {

        Multiset<Integer> this_allele_freq_spectrum =
            ImmutableMultiset.copyOf(getAlleleFreq().values());

        return new HashCodeBuilder().append(getModel()).append(
            this_allele_freq_spectrum).toHashCode();
    }

    /**
     * Creates an instance of infinite-alleles data with the given model and the cannonical string
     * representation of the data.
     *
     * @param model the data model
     * @param cannonicalForm th cannonical string representation of the data. The cannonical
     * string representation is of the form {@code f1^t1_f2^t2...} where {@code fi} and {@code ti}
     * are positive and represent the frequency domain and the alleles on that frequency domain,
     * respectively.
     * @return a new instance of {@code KC64Data} with {@code String} based alleles
     */
    public static StringKC64Data ofCannonicalForm(final KC64 model, String cannonicalForm) {

        if (cannonicalForm == null) {
            ExceptionUtil.throwIllegalArgExNull("cannonicalForm");
        }

        return new StringKC64Data(model, parseAn(cannonicalForm));
    }

    /**
     * @param cannonicalForm infinite-allele data in the cannonical form
     * @return creates a map with alleles (as keys) and their frequencies (as values)
     */
    protected static Map<String, Integer> parseAn(String cannonicalForm) {

        if (!Pattern.matches("(\\d+\\^\\d+_)*(\\d+\\^\\d+)", cannonicalForm)) {

            final String msg = NbBundle.getMessage(KC64Data.class, "Error_In_KC64_Cannonical_Form",
                                   cannonicalForm);

            throw new IllegalArgumentException(msg);
        }

        String[] fitis = cannonicalForm.split("_");    // strings of fi^ti


        // calculate fi`s and ti`s
        int[] fi = new int[fitis.length];
        int[] ti = new int[fitis.length];

        for (int i = 0; i < fitis.length; i++) {

            fi[i] = Integer.parseInt(fitis[i].split("\\^")[0]);
            ti[i] = Integer.parseInt(fitis[i].split("\\^")[1]);
        }

        Map<String, Integer> map = new HashMap<>(10);
        int type_counter         = 1;

        for (int aFi : fi) {

            for (int aTi : ti) {

                map.put("Type-" + type_counter++, aFi);
            }
        }

        return map;
    }

    /**
     * Creates an instance of infinite-alleles data with the given model and allele frequencies.
     *
     * @param model the data model
     * @param alleleFreq allele frequency
     * @return a new instance of {@code KC64Data} with {@code String} based alleles
     */
    public static KC64Data<String, KC64, ?> of(final KC64 model, Map<String, Integer> alleleFreq) {
        return new StringKC64Data(model, alleleFreq);
    }

    /**
     * Class description
     *
     *
     * @version        Enter version here..., 12/10/09
     * @author         Susanta Tewari
     */
    public static class StringKC64Data extends KC64Data<String, KC64, StringKC64Data> {

        /**
         * Initializes the model and the allele frequencies. The allele frequency map is made
         * unmodifiable.
         *
         * @param model the data model
         * @param alleleFreq allele frequency
         * @throws NullPointerException if any of the parameters is {@code null}
         * @throws IllegalArgumentException if {@code alleleFreq} is empty
         */
        StringKC64Data(KC64 model, Map<String, Integer> alleleFreq) {
            super(model, alleleFreq);
        }

        /**
         * Method description
         *
         *
         * @return
         */
        @Override
        public MutablePopGenData<String, KC64, StringKC64Data> getMutable() {

            return new MutablePopGenData<String, KC64, StringKC64Data>(getModel(),
                                         getAlleleFreq()) {

                @Override
                public StringKC64Data getImmutable() {
                    return new StringKC64Data(getModel(), getAlleleFreq());
                }
            };
        }

        /**
         * Method description
         *
         *
         * @param data
         *
         * @return
         */
        @Override
        public StringKC64Data factory(StringKC64Data data) {
            return new StringKC64Data(data.getModel(), data.getAlleleFreq());
        }
    }
}
